package cn.itcast.protocol;

import cn.itcast.message.Message;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;


/**
 *   基于Netty的自定义协议
 *   我们也可以自己自定义协议 自定义的协议一般需要这几个字段  我们到时候自己搞个自定义协议来做个通信的聊天室
 *   一般协议规则如下 我们就按照这个格式来做一个自定义协议的一个编解码器
 *   魔数: 用来判定是否是无效数据包 其实就是看是不是符合规范的口令呗 来跟去我们都用这个口令就可以区分是不是正确的对方了
 *   版本号: 可以支持协议的升级
 *   序列号算法: 其实就是说我们的正文是什么格式的 是json 还是xml 还是啥啥啥
 *   指令类型:  留给具体业务拓展的
 *   请求序号:  消息在网络中是分组交换的，所以需要有个序号可以排序复原消息
 *   正文长度: 发送的内容的长度
 *   正文内容: 具体发送的内容
 *
 *   我们要做一个自定义的协议 那么肯定要有一个我们自定义协议的一个编解码器
 *
 *   下面就是自定义的编解码器
 *
 *   ByteBuf是netty的一个核心 具体忘了它相关的东西的可以去看看TestByteBuf这个类 自己写的
 *
 *   不会有闭包问题(数据太多) 但是会有半包问题(数据太少) 因为数据太少可能回导致我们连基本的序列号啊版本号啊等这种都读不到
 *   会有问题 最起码都是空指针问题  看看TestMessageCodec里面看解决方案
 *
 */
@Slf4j
// @ChannelHandler.Sharable 这个在父类的设计中要求了 子类是不能写@Sharable注解的 所以这里不能用 用了会报错
//但是我们看了我们自己的代码发现并发下确实还是线程安全的鸭 我就想写这个@Sharable注解咋办 解决方案: 换个爹继承 具体看MessageCodecSharable这个类
public class MessageCodec extends ByteToMessageCodec<Message> { //编解码类型是Message

    /**
     * 编码的 出站前把你的信息编码的
     * @param ctx
     * @param msg
     * @param out
     * @throws Exception
     */
    @Override
    public void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        // 1. 4 字节的魔数
        out.writeBytes(new byte[]{1, 2, 3, 4}); //writeBytes从ByteBuf的可写的位置开始写入一个字节数组 内容是1234
        // 2. 1 字节的版本,
        out.writeByte(1);//writeByte从ByteBuf的可写的位置开始开始写入一个字节 内容是1
        // 3. 1 字节的序列化方式 jdk序列化/反序列化 0 , json格式 1
        out.writeByte(0);
        // 4. 1 字节的指令类型
        out.writeByte(msg.getMessageType());
        // 5. 4 个字节的请求序号
        out.writeInt(msg.getSequenceId()); ////writeInt从ByteBuf的可写的位置开始开始写入一个Int (4个字节)
        // 无意义，对齐填充 因为除了内容以外的这种协议规范目前总共有15个字节 但是在协议的规范中一般得是2的整数倍 所以我们这里加一个让其规范
        out.writeByte(0xff);
        // 6. 获取内容的字节数组
        //一个对象怎么变成二进制数据? 只要一个对象实现了序列化接口他在网络传输肯定就直接转换为二进制了
        //这里我们利用两个类，把一个对象直接写出成二进制数据byte数组 前提是这个类得实现序列化接口
        //序列化
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(msg);
        byte[] bytes = bos.toByteArray();
        // 7. 长度
        out.writeInt(bytes.length);
        // 8. 写入内容
        out.writeBytes(bytes);
    }

    /**
     * 解码的 入站前把数据解码的
     * @param ctx
     * @param in
     * @param out
     * @throws Exception
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        int magicNum = in.readInt();
        byte version = in.readByte();
        byte serializerType = in.readByte();
        byte messageType = in.readByte();
        int sequenceId = in.readInt();
        in.readByte();
        int length = in.readInt();
        byte[] bytes = new byte[length];
        in.readBytes(bytes, 0, length);
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        Message message = (Message) ois.readObject();
        log.debug("{}, {}, {}, {}, {}, {}", magicNum, version, serializerType, messageType, sequenceId, length);
        log.debug("{}", message);
        out.add(message);
    }
}